/*
 * Copyright (C) 2008 Peter Kling
 *
 * This file is part of 'A Kind of Billiard', a graphical billiard simulation.
 *
 * 'A Kind of Billiard' is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 *
 * 'A Kind of Billiard' is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * 'A Kind of Billiard'. If not, see <http://www.gnu.org/licenses/>.
 */

package cg2.billiard;

import java.util.ArrayList;

import javax.media.opengl.GL;

import cg2.mathematics.MathVector;
import cg2.mathematics.Mathematics;
import cg2.misc.OpenGLDrawable;
import cg2.models.GLModel;
import cg2.physics.PhysicalBall;
import cg2.physics.PhysicalObject;
import cg2.physics.Physics;
import cg2.physics.SimpleObstacle;

/**
 * @brief Super class for billiard tables.
 * 
 * Child classes can use a custom model by setting the display list appropriately. If it is not set, the billiard table
 * will be drawn as a green polygon without feet/borders or similar goodies. Furthermore, an indicator for the direction
 * and force the cue ball will be played with is drawn. Subclasses can adjust this by overwriting drawCueIndicator().
 */
public abstract class BilliardTable implements OpenGLDrawable {
	// geometric table data
	protected ArrayList<SimpleObstacle> planes = new ArrayList<SimpleObstacle>();
	protected double length;
	protected double width;
	protected double height;
	
	// model for table
	protected GLModel model;
	
	// cue indicator
	protected CueIndicator cueIndicator = null;
	
	// ball data
	protected BilliardBall            cueBall        = null;
	protected ArrayList<BilliardBall> balls          = new ArrayList<BilliardBall>();
	protected boolean                 gravityEnabled = false;
	
	// thread that handles collision detection and ball movement
	protected Thread physicsThread;
	
	/**
	 * @brief Constructs a billiard table.
	 */
	public BilliardTable() {
	}
	
	/**
	 * @brief A stroke with the cue using the current cue direction and cue velocity.
	 */
	public void cueStroke() {
		if (cueBall != null)
			cueBall.setVelocity(cueIndicator.getCueStrokeVelocity());
	}
	
	/**
	 * @brief Decrease the velocity the cue ball will be played with.
	 */
	public void decreaseCueVelocity() {
		cueIndicator.decreaseCueVelocity();
	}
	
	/**
	 * @brief Draws the billiard table and its balls.
	 */
	public void draw(GL gl) {
		// draw the table		
		if (model == null) {
			gl.glBegin(GL.GL_QUADS);
			gl.glVertex3d(-length/2., height, width/2.);
			gl.glVertex3d( length/2., height, width/2.);
			gl.glVertex3d( length/2., height, -width/2.);
			gl.glVertex3d(-length/2., height, -width/2.);
			gl.glEnd();
		} else {
			model.draw(gl);
		}
	
		// draw balls
		gl.glPushMatrix();
		gl.glTranslated(0., height, 0.);
		cueBall.draw(gl);
		for (BilliardBall currBall : balls)
			currBall.draw(gl);
		gl.glPopMatrix();
	}
	
	/**
	 * @brief Returns the cue ball.
	 */
	public BilliardBall getCueBall() {
		return cueBall;
	}
	
	/**
	 * @brief Returns the cue indicator of this table.
	 */
	public CueIndicator getCueIndicator() {
		return cueIndicator;
	}
	
	/**
	 * @brief Returns whether ball gravity is enabled or not.
	 */
	public boolean getGravityEnabled() {
		return gravityEnabled;
	}
	
	/**
	 * @brief Returns the height of the billiard table.
	 */
	public double getHeight() {
		return height;
	}
	
	/**
	 * @brief Checks for and handles collisions between any pair of balls.
	 */
	protected void handleBallCollisions() {
		for (int i=0; i<balls.size(); i++) {
			if (cueBall != null)
				Physics.handleCollision(balls.get(i), cueBall);
			for (int j=i+1; j<balls.size(); j++)
				Physics.handleCollision(balls.get(i), balls.get(j));
		}
	}
	
	/**
	 * @brief Checks for and handles collisions between balls and borders.
	 */
	protected void handleBorderCollisions() {
		if (cueBall != null) {
			for (SimpleObstacle currPlane : planes) {
				Physics.handleCollision(cueBall, currPlane);
			}
		}
		for (PhysicalBall currBall : balls) {
			for (SimpleObstacle currPlane : planes) {
				Physics.handleCollision(currBall, currPlane);
			}
		}
	}
	
	/**
	 * @brief Increases the velocity the cue ball will be played with.
	 */
	public void increaseCueVelocity() {
		cueIndicator.increaseCueVelocity();
	}
	
	/**
	 * @brief Initializes the billiard table. 
	 */
	public void init(GL gl){		
		// initialize table and balls
		initBorderPlanes();
		initBalls();
		initTable_model(gl);
		initBalls_models(gl);
		cueIndicator = new CueIndicator(this); cueIndicator.init(gl);
		
		// create and start physics thread for ball movement and collision handling
		physicsThread = new Thread() {
			public void run() {
				for(;;) {
					handleBorderCollisions();
					handleBallCollisions();
					for (PhysicalObject ball : balls)
						ball.update();
					if (cueBall != null)
						cueBall.update();
				}
			}
		};
		physicsThread.setPriority(Thread.MAX_PRIORITY);
		physicsThread.start();
		
		// start balls (initially paused)
		for (PhysicalObject ball : balls)
			ball.resume();
		if (cueBall != null)
			cueBall.resume();
		
		// initialize gravity due to the current gravity state
		initBalls_gravity();
	}
	
	/**
	 * @brief Initializes the billiard balls.
	 * 
	 * Use this method to load a custom set of balls.
	 */
	protected abstract void initBalls();
	
	/**
	 * @brief Initializes the gravity for billiard balls.
	 * 
	 * Sets the gravity of the balls according to the gravityEnabled member variable.
	 */
	protected void initBalls_gravity() {
		if (gravityEnabled == true) {
			MathVector gravity = Mathematics.mul(Mathematics.getUnitVector(3, 2), -Physics.gravity_moon);
			if (cueBall != null)
				cueBall.setAcceleration(gravity);
			for (PhysicalObject ball : balls)
				ball.setAcceleration(gravity);
		} else {
			if (cueBall != null)
				cueBall.setAcceleration(0., 0., 0.);
			for (PhysicalObject ball : balls)
				ball.setAcceleration(0., 0., 0.);
		}
	}
	
	/**
	 * @brief Initializes the models for the billiard balls.
	 * 
	 * Overwrite this method to load custom ball models. If this method is not overwritten, the balls will be drawn
	 * using the current OpenGL color.
	 */
	protected void initBalls_models(GL gl) {
	}
	
	/**
	 * @brief Initializes the model for the billiard table.
	 * 
	 * Overwrite this method to load a custom table model. If this method is not overwritten, the table will be drawn
	 * as a green plane in the air.
	 */
	protected void initTable_model(GL gl) {
	}
	
	/**
	 * @brief Initializes the planes corresponding to the borders of the table.
	 * 
	 * This default implementation will just define 5 border planes (left, right, front, back, bottom). Their will be
	 * no 'holes' for the pockets (the geometry depends on the concrete billiard table implemented). Overwrite this
	 * method and add holes at positions suitable for the implemented billiard table.
	 * 
	 * Default: plane0: left; plane1: front; plane2: right; plane3: back, plane4: lower
	 */
	protected void initBorderPlanes() {
		MathVector normal = new MathVector(3);
		
		// plane for left border
		normal.set(0, -1.); normal.set(1, 0.); normal.set(2, 0.);
		planes.add(new SimpleObstacle(normal, length/2.));
		
		// plane for front border
		normal.set(0, 0.); normal.set(1, 0.); normal.set(2, 1.);
		planes.add(new SimpleObstacle(normal, width/2.));
		
		// plane for right border
		normal.set(0, 1.); normal.set(1, 0.); normal.set(2, 0.);
		planes.add(new SimpleObstacle(normal, length/2.));
		
		// plane for back border
		normal.set(0, 0.); normal.set(1, 0.); normal.set(2, -1.);
		planes.add(new SimpleObstacle(normal, width/2.));
		
		// plane for lower border
		normal.set(0, 0.); normal.set(1, -1.); normal.set(2, 0.);
		planes.add(new SimpleObstacle(normal, 0.));
	}
	
	/**
	 * @brief Pauses the table (movement of balls and collision handling).
	 * 
	 * This method will not stop the balls. Thus, on resuming the balls will behave as if they had moved all the time
	 * (but without collision handling --> if pause is too long, moving balls will probably vanish). Use this method
	 * only if you want to stop movement for a very short time.
	 * 
	 * @see resume()
	 */
	public void pause() {
		if (cueBall != null)
			cueBall.pause();
		for (PhysicalObject ball : balls)
			ball.pause();
	}
	
	/**
	 * @brief Pauses the table (movement of balls and collision handling).
	 * 
	 * This method will stop the balls. Upon resuming, the balls will behave as if no time had elapsed.
	 * 
	 * @see resume()
	 */
	public void pause_time() {
		if (cueBall != null)
			cueBall.pause_time();
		for (PhysicalObject ball : balls)
			ball.pause_time();
	}
	
	/**
	 * @brief Resets the billiard table.
	 * 
	 * The standard implementation will reset the cue indicator, but not the ball positions (it does not know the exact
	 * geometry of the implemented billiard table). Overwrite this method and reset the ball positions manually.
	 * Additionally, the velocity of the balls is set to zero, so they stop moving. The acceleration of the balls is set
	 * due to the current state of the gravity.
	 */
	public void reset() {
		cueIndicator.reset();
		pause();
		if (cueBall != null)
			cueBall.setVelocity(0., 0., 0.);
		for (BilliardBall ball : balls)
			ball.setVelocity(0., 0., 0.);
		initBalls_gravity();
		resume();
	}
	
	/**
	 * @brief Resets the cue ball of the billiard table
	 * 
	 * Use this method to reset the cue balls position. This default implementation will only reset the cue indicator
	 * and stop any ball movement. Overwrite it in subclasses to reset for example the position of the cue ball.
	 */
	public void resetCueBall() {
		cueIndicator.reset();
		pause();
		if (cueBall != null)
			cueBall.setVelocity(0., 0., 0.);
		for (BilliardBall ball : balls)
			ball.setVelocity(0., 0., 0.);
		initBalls_gravity();
		resume();
	}
	
		
	/**
	 * @brief Resumes the table (movement of balls and collision handling).
	 */
	public void resume() {
		for (PhysicalObject ball : balls)
			ball.resume();
		if (cueBall != null)
			cueBall.resume();
	}
	
	/**
	 * @brief Increases the angles for horizontally and vertically rotations of the cue by the given values.
	 */
	public void rotateCue(double angleH, double angleV) {
		cueIndicator.rotateCue(angleH, angleV);
	}
	
	/**
	 * @brief Rotates the cue some degree 'downwards'.
	 * 
	 * Using this method you can rotate the cue such that the ball is played upwards up to some maximum angle.
	 */
	public void rotateCue_down() {
		cueIndicator.rotateCue_down();
	}
	
	/**
	 * @brief Rotates the cue some degree around the y-axis in mathematical positive sense.
	 */
	public void rotateCue_left() {
		cueIndicator.rotateCue_left();
	}
	
	/**
	 * @brief Rotates the cue some degree around the y-axis in mathematical negative sense.
	 */
	public void rotateCue_right() {
		cueIndicator.rotateCue_right();
	}
	
	/**
	 * @brief Rotates the cue some degree 'upwards'.
	 * 
	 * Using this method you can rotate the cue such that the ball is played downwards up to some maximum angle.
	 */
	public void rotateCue_up() {
		cueIndicator.rotateCue_up();
	}
	
	/**
	 * @brief Sets the velocity the cue ball will have after the next stroke with the cue.
	 */
	public void setCueVelocity(double value) {
		cueIndicator.setCueVelocity(value);
	}
	
	/**
	 * @brief Switches between enabled/disabled gravity for the balls.
	 */
	public void toggleGravity() {
		gravityEnabled = !gravityEnabled;
		initBalls_gravity();
	}
}
